Previous Page TOC Next Page


7 — Events

by Bill Hatfield

Visual Basic is a Visual Development Environment (VDE). In fact, it was the first VDE for Windows. A VDE is more than a programming language. It is a complete environment tailored to the needs of a programmer writing applications for a Graphical User Interface (GUI) environment.

If you have never programmed in a VDE and you are new to Visual Basic, then this new way of doing things will take some getting used to. Programming in Visual Basic is a very different experience from programming in a traditional language like C, Pascal, or COBOL. Probably the biggest difference between the two is the event-driven paradigm. Let me explain.

Introducing the Event-Driven Paradigm

In traditional development environments, you, as the developer, were in complete control. You determined what happened first and then what happened next. That's not to say that the users didn't have input. They did, of course. But for the most part, if the application was at step three, for example, you knew for sure that steps one and two already had been completed. You could depend on it because there was no way the user could have gotten to step three without going through the first two steps. This sort of certainty helped to simplify coding and error checking.

An application that makes use of the event-driven model works very differently. It starts up by doing some initialization code, just as the traditional system did, but at some point the application just stops. It doesn't do anything. The user stares at the application and the application stares back. And nothing happens until the user makes a move. What kind of move? Well, nearly anything qualifies. She could simply move her mouse. Or she could click a menu or a button. When she does, she sets into motion a series of events that cause the application to respond.

An Example

Let's say the user clicks a button. The operating system is monitoring very closely every move the user makes. So it doesn't fail to notice the click. The operating system also notices that the clicked button is a part of the Visual Basic application you wrote, so it sends a message to this application. Message here is a technical term, but it means pretty much what you might think. It is as if Windows takes out its quill pen and writes a brief note that says,

Dear VB Application,

The user has just clicked on the OK button that is on your main window.

Love,

Windows

It then drops it in the mailbox to be delivered to the application. A couple of milliseconds later when your application reads the message, it responds by generating an event on the appropriate object. The object, in this case, is the button on the main window. And the event is Click.

Now it just so happens that when the application was written, the programmer had an inkling that the user might click on that button. So he wrote a routine that was associated with the Click event of the OK button on the main window. In fact, that is how you will do almost all your programming in an event-driven application. You first choose an object. Then you choose an event that can happen to that object. And finally, you write a routine that will be executed at runtime when that event happens to that object.

Objects and Events

What are some examples of objects? The button in the earlier example was an object. But if there are text boxes, labels, drop-down listboxes, and other controls on the window, all of those are objects as well. Even the window itself is an object.

Events are simply things that can happen to an object. A given object can have many events. A text box can receive the focus (the GotFocus event). And it can lose the focus (LostFocus). Its text can be changed (Changed). It even can be clicked on like the button was (Click). All these are events associated with a text box control. A button shares some of these events (like Click, GotFocus, and LostFocus) but not all of them. And the button may even have a few of its own events that the text box doesn't have. Table 7.1 summarizes some often-used events common to many objects.

Event


Event Description


Change

Occurs again and again as changes are made in a text control.

Click

Occurs when the user presses and then releases a mouse button over an object.

DblClick

Occurs when the user rapidly presses the mouse button twice.

DragDrop

Occurs when the user presses and holds the mouse button down over a dragable object, moves the mouse to a new position, and then releases the mouse button (drops the dragged object). The DragDrop event is an event of the object that was dropped on, not the one being dragged.

DragOver

Occurs in an object when another object is being dragged across it.

GotFocus

Occurs when an object receives the focus. Happens when the user tabs to or clicks to move focus to the object.

KeyDown, KeyUp

Occurs when the user presses (KeyDown) or releases (KeyUp) a key while an object has the focus.

KeyPress

Occurs when the user presses and releases a key.

LostFocus

Occurs when an object loses the focus. Happens when a user tabs to or clicks on another object.

MouseDown, MouseUp

Occurs when the user presses (MouseDown) or releases (MouseUp) a mouse button.

MouseMove

Occurs when the user moves the mouse across the screen. MouseMove is generated again and again dozens and dozens of times as the mouse moves from one place to another.

Will you, as a programmer, write code to respond to all these different events? No. In fact, you probably will write code only for a small portion of them. You will simply write for the events you think should be responded to in your application.

Do all events happen because the user did something? No. Although most events ultimately happen because of user intervention, Windows itself also can generate events based on a timer, the system clock, or the changing status of a connected peripheral.

A Shift of Control

But wait. If all your code is in these little event routines and the routines are executed only when the associated event happens, then how do you know which events will happen first? The answer is simple: You don't. And that's why event-driven programming can be more challenging than traditional development. If you are in step three, you have to check to see whether steps one and two have been done. If not, you have to decide whether they really must be done before step three. If they do, you will have to inform your users. If not, let your users do it however they like.

This points to a subtle but very important shift in priorities. You, the developer, are no longer the one in control. The user is. In fact, the user should be the center of the universe you create. This concept, called user-centric design, is the cornerstone to developing truly great user interfaces.

Looking at Event-Driven Gotchas

In the event-driven world, there are times when you can be writing code that looks completely harmless but causes problems when run. Look at the window in Figure 7.1.


FIGURE 7.1. A simple Visual Basic form.

The first text box is txtName and the second is txtPhone. Now suppose that the code in the LostFocus event of the first text box looks like Listing 7.1.

Private Sub txtName_LostFocus()

If txtName.Text = "" Then

    Beep

    txtName.SetFocus

End If

End Sub

Now suppose that the LostFocus event of the second text box looks like Listing 7.2.

Private Sub txtPhone_LostFocus()

If txtPhone.Text = "" Then

    Beep

    txtPhone.SetFocus

End If

End Sub

Looks harmless enough. If either of these is empty, you will want your application to beep when users try to tab away and force them to enter something in the text box.

But if you run this code and simply press Tab right away, what will happen? First, the focus moves to txtPhone. Then the LostFocus event of txtName is executed (because it just lost the focus to txtPhone). txtName's LostFocus checks to see whether txtName is empty. Since txtName is empty, it beeps and sets the focus back on txtName. So far so good. But setting the focus back on txtName causes txtPhone to trigger its LostFocus event. It goes through the same process and tries to set the focus back to it. This, in turn, triggers the txtName LostFocus event, creating an endless loop.

This example shows how easy it can be to unintentionally create bad situations in event-driven code. Remember that everything you do in code has the potential for triggering other events, and if those events trigger events that eventually trigger your code again, you easily can create an endless loop.

The moral of this particular story is this: Be careful when using a SetFocus command in a LostFocus or GotFocus event. What are some other situations to watch out for?

Table 7.2 lists some actions and corresponding events likely to be triggered.

Action


Event(s) Triggered


Form Refresh

form's_Paint

Changing the Text property of a text box

text box's Change

Changing the Height or Width properties of a form

form's_Paint and form's_Resize

If you perform any of these actions in the event script of an event they could trigger, you risk an endless loop. If you change the height of the form in the form's Resize event, for example, you almost certainly will cause an endless loop that will generate an Out of Stack Space error. Again, these are just a few common examples. If you get an Out of Stack Space error or some other form of endless loop, be sure to check for this kind of situation.

Application Startup

So if our application is event-driven and code is only written in response to events, how do things ever get started? How your application starts depends on the setting in the Options dialog box. To access the Options dialog box, choose Options from the Tools menu. The Options tabbed dialog box appears. Now choose the Project tab. The first control is Startup Form.

Visual Basic assumes that the first thing you want to do is display a form. And, of course, this usually is correct. It further assumes that the first form you create is the one you will want to display when your application starts. This is much more likely to be a wrong assumption. After you have created several forms in your application and then want to test it, be sure to come here to the Options dialog and verify that the correct form is specified.

But what if you don't want a window to display first thing? Suppose that you want to do some initialization or checking of the system before you actually display a form? If you click on the Startup Form drop-down listbox, you will see that one of the options is Sub Main. This option enables you to create a subroutine named Main and to have it be the first thing executed. After it finishes its processing, it probably will show a form or two, but executing the subroutine first gives you a chance to verify that everything is okay before you get started.

You also would use Sub Main if you wanted to create a batch application that does all its processing behind the scenes without any user interaction. This kind of application is more unusual, but it is always good to have the option.

Application Shutdown

The next logical question is how does this whole thing end? The simplest answer is that it ends when the last form of your application is unloaded. That's the way it looks from the user's perspective. But the fact is that you still can have code executing behind the scenes long after the user thinks your application has died. So it is better to say that the application ends when the last of its components has left memory.

But that isn't very helpful from a programming perspective. How do you wrap up processing for an application in your code? The Unload command removes a form from memory. Assuming that nothing else is running, an Unload on the last form that is up shuts down the application. So generally, you don't have to worry about anything special when you are creating a simple application. Just have each form do its own cleanup in the form's Unload event.

What about larger MDI applications? Here is the process that takes place when the user indicates that he wants to close a MDI frame:

  1. All the MDI child windows receive a QueryUnload event, each in turn, followed by the MDI frame's QueryUnload. In this event, each of the windows can decide whether it is OK for the application to close. This event often is used to query the user to see whether she wants to save data or cancel the close. Any of the windows can choose to cancel the close. And, in doing so, all the sheets and the frame stay open.

  2. If none of the child windows cancel to the close, the Unload event occurs for each child window, and then it closes.

  3. After all the child windows are closed, the frame receives an Unload event and subsequently closes. Because the MDI frame is usually the last window up at this stage, the application ends.


NOTE

Can't you cancel the Unload in the Unload event? Actually, yes, you can. But if you do it there, you won't know the current state of the application. Some or all of the other child windows already may have been unloaded by that time. Using the QueryUnload for canceling ensures that everything is left as it was.

What if you don't want to go through all this checking and verifying? You can stop your application cold in its tracks by executing the End command.


WARNING

Be careful with End. It kills the application right away. No QueryUnload—not even an Unload event—occurs. So if you have housekeeping chores or any other code in the Unload of your forms, it will not be executed.

Another way to freeze your program is by using the Stop command. This is a break. It differs from End in that it leaves all your variable values intact so that you can use the Debug window to check them. You even can restart after a Stop. You will use Stop a lot in debugging, but your final code probably will never have a need to use it.

Digging Deeper Mapping Visual Basic Events to Windows Messages

So Visual Basic creates a visual, easy-to-use way of creating Windows applications. It receives Windows messages and converts them into events for which you can write code. But sooner or later, you are going to want to go further with your applications. Sometimes you will hear developers talk about "hitting walls" with environments like Visual Basic. Although Visual Basic makes it easy to access the most common capabilities of Windows, it doesn't really erect many walls that prevent you from going deeper. You just have to know how it's done.

Suppose that you want to make use of a Windows message that is not captured in Visual Basic. The first step is to be absolutely sure that it isn't captured in some way. Although many messages are captured as form or control events, some are captured through methods as well. Table 7.3 lists window messages and how they are captured within Visual Basic.

Windows Message


VB Event/Method/Property


Description


WM_ACTIVATE

Form_Activate

Indicates a change in the activation state

WM_ACTIVATEAPP

None

Notifies applications when a new task is activated

WM_ASKCBFORMATNAME

GetFormat() Clipboard function

Retrieves the name of the Clipboard format

WM_CANCELMODE

None

Notifies a window to cancel internal modes

WM_CHANGECBCHAIN

None

Notifies Clipboard viewer of removal from chain

WM_CHAR

Form_KeyPress, Form_KeyDown

Passes keyboard events to focus window

WM_CHARTOITEM

None, managed by Visual Basic

Provides listbox keystrokes to owner window

WM_CHILDACTIVATE

MDIChildForm_ Activate

Notifies a child window of activation

WM_CHOOSEFONT_GETLOGFONT

None, managed by Visual Basic

Retrieves LOGFONT structure for Font dialog box

WM_CLEAR

Used as the control object.Clear method or Text1.Text = ""

Clears an edit or combo box

WM_CLOSE

Form_Unload,Form_QueryUnload

Signals a window or application to terminate

WM_COMMAND

object_Click (menu or command button)

Specifies a command message

WM_COMMNOTIFY

None

Notifies a window about the status of its queues

WM_COMPACTING

None

Indicates a low-memory con-dition

WM_COMPAREITEM

Used as the object.ListIndex method

Determines position of combo box or listbox item.

WM_COPY

Same as SetText() and SetData() Clipboard methods

Copies a selection to the Clipboard

WM_CREATE

Form_Load

Indicates that a window is being created

WM_CTLCOLOR

None

Indicates that a control is about to be drawn

WM_CUT

Used as SetText() or SetData() Clipboard methods

Deletes a selection and copies it to the Clipboard

WM_DDE_ACK


Acknowledges the receipt of a DDE transaction

WM_DDE_EXECUTE

object.LinkExecute()

Passes a command to a DDE server

WM_DDE_INITIATE

object.LinkMethod()

Initiates a DDE conversation

WM_DDE_POKE

object.LinkPoke()

Sends an unsolicited data item to a server

WM_DDE_REQUEST

object.LinkRequest()

Requests value of a data item from a DDE server

WM_DDE_TERMINATE

object.LinkClose()

Terminates a DDE conversation

WM_DEADCHAR

None

Indicates when a dead key is pressed

WM_DELETEITEM

None

Indicates that owner-drawn item or control is altered

WM_DESTROY

Form_QueryUnload

Indicates that window is about to be destroyed

WM_DESTROYCLIPBOARD

None

Notifies owner when Clipboard is emptied

WM_DEVMODECHANGE

None

Indicates when device-mode settings are changed

WM_DRAWCLIPBOARD

None

Indicates when Clipboard contents are changed

WM_DRAWITEM

None

Indicates when owner-drawn control or menu changes

WM_DROPFILES

object_DragDrop

Indicates when a file is dropped

WM_ENABLE

None

Indicates when enable state of window is changing

WM_ENDSESSION

None

Indicates whether the Windows session is ending

WM_ENTERIDLE

None

Indicates that a modal dialog box or menu is idle

WM_ERASEBKGND

None, closest item is the AutoRedraw property

Indicates when a window background needs erasing

WM_FONTCHANGE

None

Indicates a change in the font-resource pool

WM_GETDLGCODE

None

Allows processing of control input

WM_GETFONT

None

Retrieves the font that a control is using

WM_GETMINMAXINFO

None, however can be linked to Form_Resize

Retrieves minimum and maximum sizing information

WM_GETTEXT

Used as GetText() or GetData() Clipboard methods

Copies the text to a corresponding window

WM_GETTEXTLENGTH

Same as Len(object.Text)

Determines length of text associated with a window

WM_HSCROLL

object.Scroll where object is a horizontal scroll bar

Indicates a click in a horizontal scroll bar

WM_HSCROLLCLIPBOARD

None

Prompts owner to scroll Clipboard contents

WM_ICONERASEBKGND

None

Notifies minimized window to fill icon background

WM_INITDIALOG

None

Initializes a dialog box

WM_INITMENU

None

Indicates when a menu is about to become active

WM_INITMENUPOPUP

Used as object.PopUpMenu method

Indicates when a pop-up menu is being created

WM_KEYDOWN

object_KeyDown

Indicates when a nonsystem key is pressed

WM_KEYUP

object_KeyUp

Indicates when a nonsystem key is released

WM_KILLFOCUS

object_LostFocus

Indicates window is about to lose input focus

WM_LBUTTONDBLCLK

object_DblClick

Indicates double-click of left mouse button

WM_LBUTTONDOWN

object_MouseDown

Indicates when left mouse button is pressed

WM_LBUTTONUP

object_MouseUp

Indicates when left mouse button is released

WM_MBUTTONDBLCLK

object_DblClick

Indicates double-click of middle mouse button

WM_MBUTTONDOWN

object_MouseDown

Indicates when middle mouse button is pressed

WM_MBUTTONUP

object_MouseUp

Indicates when middle mouse button is released

WM_MDIACTIVATE

MDIChildForm Activate

Activates a new MDI child window

WM_MDICASCADE

Used as the MDIForm.Arrange() method

Arranges MDI child windows in a cascade format

WM_MDICREATE

None

Prompts a MDI client to create a child window

WM_MDIDESTROY

MDIChildForm QueryUnload, MDIChildForm_Unload

Closes a MDI child window

WM_MDIGETACTIVE

None

Retrieves data about the active MDI child window

WM_MDIICONARRANGE

MDIForm.Arrange()

Arranges minimized MDI child windows

WM_MDIMAXIMIZE

None

Maximizes a MDI child window

WM_MDINEXT

None

Activates the next MDI child window

WM_MDIRESTORE

None

Prompts a MDI client to restore a child window

WM_MDISETMENU

None

Replaces the menu of a MDI frame window

WM_MDITILE

MDIForm.Arrange()

Arranges MDI child windows in a tiled format

WM_MEASUREITEM

None

Requests dimensions of owner-drawn control

WM_MENUCHAR

None

Indicates when unknown menu mnemonic is pressed

WM_MENUSELECT

MenuObject_Click

Indicates when a menu item is selected

WM_MOUSEACTIVATE

Form_Activate

Indicates a mouse click in an inactive window

WM_MOUSEMOVE

object_MouseMove

Indicates mouse-cursor movement

WM_MOVE

None

Indicates the position of a window has changed

WM_NCACTIVATE

None. Visual Basic does not provide access to any non-client events

Changes the active state of a nonclient area

WM_NCCALCSIZE

Used as the ScaleWidth and ScaleHeight properties

Calculates the size of a window's client area

WM_NCCREATE

None

Indicates that a nonclient area is being created

WM_NCDESTROY

None

Indicates when nonclient area is being destroyed

WM_NCHITTEST

None

Indicates mouse-cursor movement

WM_NCLBUTTONDBLCLK

None

Indicates non-client left button double-click

WM_NCLBUTTONDOWN

None

Indicates left button pressed in nonclient area

WM_NCLBUTTONUP

None

Indicates left button released in nonclient area

WM_NCMBUTTONDBLCLK

None

Indicates middle button nonclient double-click

WM_NCMBUTTONDOWN

None

Indicates middle button pressed in nonclient area

WM_NCMBUTTONUP

None

Indicates middle button released in nonclient area

WM_NCMOUSEMOVE

None

Indicates mouse-cursor movement in nonclient area

WM_NCPAINT

None

Indicates that a window's frame needs painting

WM_NCRBUTTONDBLCLK

None

Indicates right button nonclient double-click

WM_NCRBUTTONDOWN

None

Indicates right button pressed in nonclient area

WM_NCRBUTTONUP

None

Indicates right button released in nonclient area

WM_NEXTDLGCTL

Used as the SetFocus() method

Sets focus to a different dialog box control

WM_PAINT

Form_Paint

Indicates that a window frame needs painting

WM_PAINTCLIPBOARD

None

Paints the specified portion of the window

WM_PALETTECHANGED

None

Indicates that focus window has realized its palette

WM_PALETTEISCHANGING

None

Informs windows about change to palette

WM_PARENTNOTIFY

None

Notifies parent of child-window activity

WM_PASTE

Used as Text1.Text =Clipboard.GetText (vbCFText)

Inserts Clipboard data into an edit control (text box)

WM_POWER

None

Indicates that the system is entering suspended mode

WM_QUERYDRAGICON

None

Requests a cursor handle for a minimized window

WM_QUERYENDSESSION

None

Requests that the Windows session be ended

WM_QUERYNEWPALETTE

None

Enables a window to realize its logical palette

WM_QUERYOPEN

None

Requests that a minimized window be restored

WM_QUEUESYNC

None

Delimits CBT messages

WM_QUIT

Form_QueryUnload, Form_Unload

Requests that an application be terminated

WM_RBUTTONDBLCLK

object_DblClick

Indicates a double-click of right mouse button

WM_RBUTTONDOWN

object_MouseDown

Indicates when the right mouse button is pressed

WM_RBUTTONUP

object_MouseUp

Indicates when the right mouse button is released

WM_RENDERALLFORMATS

None

Notifies owner to render all Clipboard formats

WM_RENDERFORMAT

None

Notifies owner to render particular Clipboard data

WM_SETCURSOR

object.ScreenMouse Pointer property

Displays the appropriate mouse cursor shape

WM_SETFOCUS

object_GotFocus

Indicates when a window has gained input focus

WM_SETFONT

Used as the object.FontName property

Sets the font for a control

WM_SETREDRAW

None

Allows or prevents redrawing in a window

WM_SETTEXT

Used as the .Text or .Caption

Sets the text of a window properties

WM_SHOWWINDOW

None

Indicates that a window is about to be hidden or shown

WM_SIZE

Form_ReSize

Indicates a change in window size

WM_SIZECLIPBOARD

None

Indicates a change in Clipboard size

WM_SPOOLERSTATUS

None

Indicates when a print job is added or removed

WM_SYSCHAR

None

Indicates when a system-menu key is pressed

WM_SYSCOLORCHANGE

None

Indicates when a system color setting is changed

WM_SYSCOMMAND

None

Indicates when a system command is requested

WM_SYSDEADCHAR

None

Indicates when a system dead key is pressed

WM_SYSKEYDOWN

object_KeyDown

Indicates that Alt plus another key was pressed

WM_SYSKEYUP

object_KeyUp

Indicates that Alt plus another key was released

WM_SYSTEMERROR

None

Indicates that a system error has occurred

WM_TIMECHANGE

None

Indicates that the system time has been set

WM_TIMER

TimerObject_Timer

Indicates that the time-out interval for a timer has elapsed

WM_UNDO

None

Undoes the last operation in an edit control

WM_USER

None

Indicates a range of message values

WM_VKEYTOITEM

None

Provides listbox keystrokes to owner window

WM_VSCROLL

object_Scroll

Indicates a click where object is a vertical scroll bar

WM_VSCROLLCLIPBOARD

None

Prompts the owner to scroll Clipboard contents

WM_WINDOWPOSCHANGED

Form_Resize

Notifies a window of a size or position change

WM_WINDOWPOSCHANGING

Form_Resize

Notifies a window of a new size or position

WM_WININICHANGE

None

Notifies applications of change to WIN.INI

As you can see, some of the Windows messages map directly to events in Visual Basic. Others map directly to methods or properties. Some loosely map to an event, method, or property that Visual Basic has chosen to handle in a more generalized way. And finally, there are some that simply are unavailable. These same things could be said of the messages associated with controls in Table 7.4.

Windows Message


VB Event/Method/Property


Indicates That


BN_CLICKED

CommandButton _Click

The user clicked a button

BN_DISABLE

CommandButton.Enabled

A button is disabled

BN_DOUBLECLICKED

CommandButton _DblClick

The user double-clicked a button

BN_HILITE

CommandButton _GetFocus

The user highlighted a button

BN_PAINT

None

The button should be painted

BN_UNHILITE

CommandButton_LostFocus

The highlight should be removed

CBN_CLOSEUP

None

The listbox of a combo box has closed

CBN_DBLCLK

None

The user double-clicked a string

CBN_DROPDOWN

None

The listbox of a combo box is dropping down

CBN_EDITCHANGE

ComboBox_Change

The user has changed text in the edit control

CBN_EDITUPDATE

None

Altered text is about to be dis- played

CBN_ERRSPACE

None

The combo box is out of memory

CBN_KILLFOCUS

ComboBox_LostFocus

The combo box is losing the input focus

CBN_SELCHANGE

ComboBox.ListFocus

A new combo box list item is selected

CBN_SELENDCANCEL

None

The user's selection should be canceled

CBN_SELENDOK

None

The user's selection is valid

CBN_SETFOCUS

ComboBox_GotFocus

The combo box is receiving the input focus

EN_CHANGE

TextBox_Change

The display is updated after text changes

EN_ERRSPACE

None

The edit control is out of memory

EN_HSCROLL

None

The user clicked the scroll bar

EN_KILLFOCUS

TextBox_LostFocus

The edit control is losing the input focus

EN_MAXTEXT

Text .MaxLength

The insertion is truncated

EN_SETFOCUS

TextBox_GotFocus

The edit control is receiving the input focus

EN_UPDATE

None

The edit control is about to display altered text

EN_VSCROLL

Vscroll_Scroll

The user clicked the vertical scroll bar

LBN_DBLCLK

ListBox_DblClick

The user double-clicked a string

LBN_ERRSPACE

None

The list box is out of memory

LBN_KILLFOCUS

ListBox_LostFocus

The list box is losing the input focus

LBN_SELCANCEL

None

The selection is canceled

LBN_SELCHANGE

ListBox_Change

The selection is about to change

LBN_SETFOCUS

ListBox_GetFocus

The list box is receiving the input focus

Kicking Off Your Own Events

Windows sends messages to applications indicating what the user is doing, and the application responds. I also mentioned that events can be triggered by timers or the system clock. So can you, as a developer, trigger events? Can you send a message to other windows in your own application or even other applications? Well, if you just want to execute the code associated with the clicked event of your Update button, for example, you can simply code this:

Call cmdUpdate_Click()

This doesn't actually click the button (it doesn't cause the graphic to appear depressed), but it does execute the code in the Click event.

But what if you want to go further? You actually want to send a real Windows message to a window or control—can you do it? The answer is yes, but not in native Visual Basic. You'll have to resort to Win32.

The two primary Win32 functions you will use are SendMessage() and PostMessage(). These functions both do the same thing: They ship off a message to the window or control indicated (either in this application or in another one). The difference is that SendMessage() is synchronous and PostMessage() is asynchronous. That is, SendMessage() sends the message and waits until it has arrived there and has taken effect before it returns control to your application. PostMessage(), on the other hand, simply fires off the message and immediately returns control to your application. SendMessage(), then, works more like a function call. You know it is completely finished by the time the next line executes.

PostMessage

Suppose that your application wants to shut down another application. Using PostMessage, it is easy. First you have to declare the Win32 function, as shown in Listing 7.3.

Declare Function PostMessage Lib "user32" Alias "PostMessageA"


(ByVal hWnd As Long, _ ByVal wMsg As Long, _ ByVal wParam As Integer, _ lParam As Any) As Long

hWnd identifies the window to which the message is posted. If this parameter is HWND_BROADCAST, the message is posted to all top-level windows, including disabled or invisible windows.

wMsg specifies the message to be posted.

wParam specifies 16 bits of additional message-dependent information. lParam specifies 32 bits of additional message-dependent information.

The return value is nonzero if the function is successful. The only way it would return zero is if the receiving application's message queue is full. That can happen when you send the same application many messages without giving it a chance to respond. If you run into this problem, try sprinkling a few Yield() statements around. You will use only PostMessage() with Windows. You'll see how the more flexible SendMessage() can be used with controls later.

After you declare PostMessage(), you need to declare a few other Win32 functions you'll use along the way, as shown in Listing 7.4.

Declare Function IsWindow Lib "User" (ByVal Hwnd As Integer) As Integer

Declare Function GetWindow Lib "User" (ByVal Hwnd As Integer, ByVal wCmd As


_Integer) As Integer Declare Function GetWindowLong Lib "User" (ByVal Hwnd As Integer, ByVal nIndex As _Integer) As Long Declare Function PostMessage Lib "User" (ByVal Hwnd As Integer, ByVal wMsg As _Integer, ByVal wParam As Integer, ByVal lParam As Long) As Integer Declare Function FindWindow Lib "User" (ByVal lpClassName As Any, ByVal _lpWindowName As String) As Integer Const GW_OWNER = 4 Const GWL_STYLE = -16 Const WS_DISABLED = &H8000000 Const WS_CANCELMODE = &H1F Const WM_CLOSE = &H10

Listing 7.5 is the EndTask function. It receives a window handle. It verifies that the window handle is valid and that it is not disabled. Then the WM_CANCELMODE and WM_CLOSE messages are passed with no parameters (0 and 0& in the word and long params).

Function EndTask(TargetHwnd As Integer) As Integer

    Dim X As Integer

    Dim ReturnVal As Integer

    ReturnVal = True

    If TargetHwnd = hWndMe% Or GetWindow(TargetHwnd, GW_OWNER) = hWndMe% Then

        End

    End If

    If IsWindow(TargetHwnd) = False Then

        ReturnVal = False

    Else

        If Not (GetWindowLong(TargetHwnd, GWL_STYLE) And WS_DISABLED) Then

            X = PostMessage(TargetHwnd, WM_CANCELMODE, 0, 0&)

            X = PostMessage(TargetHwnd, WM_CLOSE, 0, 0&)

            DoEvents

        End If

    End If

EndTask% = ReturnVal

End Function

Finally, Listing 7.6 is the Form Load event procedure, which uses EndTask to take down Notepad if it happens to be running when this window opens. You can use similar code wherever you need this functionality.

Sub Form_Load()

    Dim Hwnd As Integer

    Dim Y As Integer

    Hwnd = FindWindow(0&, "Notepad")

    If Hwnd = 0 Then

        MsgBox "NotePad is not running", vbInformation

        Exit Sub

    Else

        MsgBox "NotePad will now be closed", vbInformation

    End If

    Y = EndTask(Hwnd)

    If Y <> 0 Then

        MsgBox "NotePad terminated", vbInformation

    Else

        MsgBox "Error - Cannot terminate NotePad", vbInformation

    End If

End Sub

SendMessage

SendMessage() is the synchronous sibling of PostMessage(). Unlike PostMessage(), SendMessage() tends to get used frequently to send messages to windows and controls within the same application. Why would you want to send a message to a control on your own form? Well, it turns out that this is a good way to retrieve some of the functionality that Visual Basic steals away from you and to get around some of those "walls."

Here are some examples of things you can do with the appropriate SendMessage() that Visual Basic doesn't normally let you do:

Although you can implement your own code to find a string in a listbox, you don't have to. Windows already has that functionality built right in. All you have to do is access it through SendMessage()!

The declaration and parameters are very similar to PostMessage(), as shown in Listing 7.7.

Declare Function SendMessage Lib "user32" Alias "SendMessageA"


(ByVal hWnd As Long, _ ByVal wMsg As Long, _ ByVal wParam As Integer, _ lParam As Any) As Long

hWnd identifies the window to which the message will be sent. If this parameter is HWND_BROADCAST, the message is sent to all top-level windows, including disabled or invisible unowned windows.

uMsg specifies the message to be sent.

wParam specifies 16 bits of additional message-dependent information. lParam specifies 32 bits of additional message-dependent information.

Setting Tab Stops

So how do you put it to use? Well, normally, if you include a Chr$(9) (a tab character) in a string that you put in a listbox or a multiline edit, the control simply moves the following text to its next tab stop. Visual Basic gives you no way to set these tab stops to be where you want them, however. But you can with SendMessage(). Listing 7.8 shows you how.

    Dim nTabPos(3) As Integer

    Dim lResult     As Long

    Rem Define the positions

    nTabPos(0) = 10

    nTabPos(1) = 50

    nTabPos(2) = 90

    Rem Set the focus to the target listbox and fire away

    lstExample.SetFocus

    lResult = SendMessage(GetFocus(), LB_SETTABSTOPS, 0, ByVal 0&)

    lResult = SendMessage(GetFocus(), LB_SETTABSTOPS, 3, nTabPos(0))

First, an array is created and then filled in with the desired tab positions. Then an LB_SETTABSTOPS with a 0 and ByVal 0& is used to clear any existing tab stops. Finally, LB_SETTABSTOPS with the number of array elements (3) and the first array element (nTabPos(0)) actually sets the tab stops.

You can set the tab stops in a multiline text box in the same way. Just send the message EM_SETTABSTOPS instead of LB_SETTABSTOPS.

Performing Magic with Multiline Edits

Speaking of multiline text boxes, there are several useful messages you can send to a multiline text box to get information that Visual Basic does not provide. You can send the EM_GETLINECOUNT message, for example, to get the number of lines of text in a multiline text box:

lLineCount = SendMessage(GetFocus(), EM_GETLINECOUNT, 0, ByVal 0&)

In addition, you can get the text of any individual line in a multiline text box with the message EM_GETLINE. But this message is a little tricky because it requires you to pass three parameters:

Because there are only two parameters, you will have to do a little trickery. You'll put the number of the line you want in the third parameter and the string in the fourth. But in order to tell it how long the string is, you'll have to embed the number in the first two bytes of the string, as shown in Listing 7.9.

    Dim nLineNum As Integer

    Dim nLineLen As Integer

    Dim sTemp    As String

    Rem Set the max line length to 200

    nLineLen = 200

    Rem Fill The string with nulls and then paste the length to the front

    sTemp = String$(nLinLen, vbNullChar)

    sTemp = Chr$(nLinLen Mod 256) + Chr$(nLineLen \ 256) + sTemp

    sTemp = Left$(sTemp, SendMessage(GetFocus(), EM_GETLINE, nLinNum, ByVal sTemp))

Another handy message for multiline text boxes is EM_LINESCROLL, which enables you to scroll your text boxes horizontally and vertically. Just specify the number of characters to scroll in the fourth argument of the SendMessage() function. Place the number of characters to scroll horizontally in the high order word (by multiplying by 65,536) and the number of lines to scroll vertically in the low order word. See Listing 7.10.

Sub ScrollText (txtSubject As TextBox, nHorizontal As Integer, nVertical As _Integer)

    Const EM_LINESCROLL = &HB6

    Dim lResult As Long

    Dim lScroll As Long

    lScroll = (nHorizontal * 65536) + nVertical

    txtSubject.SetFocus

    lResult = SendMessage(GetFocus(), EM_LINESCROLL, 0, ByVal lScroll)

End Sub

Keep in mind that this is a relative scroll. So if you use the value 3, the text box scrolls down three lines. If you use the value —65,536 (—1 times 65,536), the text box scrolls left one char-acter.

Finding a String in a Listbox

A searching capability already is built into the listbox. But Visual Basic doesn't let you get to it. SendMessage() does. The code for FindStringInList is in Listing 7.11. It receives a string and a listbox or combo box. It returns True or False, indicating whether it succeeded in finding the string in the listbox. If it did find it, the ListIndex for the listbox or combo box is set to the place it was found.

Function FindStringInList(sString As String, lstBox As ListBox) As Boolean

    Rem This subroutine finds the string that was passed to it and then

    Rem sets the passed combo box to that value

    Const LB_ERR = —1

    Const LB_FINDSTRING = (&H400 + 16)

    Dim lResult As Long

    Dim nZero   As Integer

    lResult = SendMessage(lstBox.hWnd, LB_FINDSTRING, 0, ByVal sString)

    If lResult = LB_ERR Then

        FindStringInList = False

    Else

        FindStringInList = True

        lstBox.ListIndex = lResult

    End If

End Function
Dropping Down a Listbox Automatically

Here's a neat trick for a drop-down list combo box (a combo box with the Style property set to 2). This code drops the list automatically when the combo box gets the focus (see Listing 7.12).

Sub Combo1_GotFocus ()

    Rem When this control receives focus it will

    Rem automatically drop down

    Dim lResult As Long

    Const CB_SHOWDROPDOWN = WM_USER + 15

    lResult = SendMessage(GetFocus(), CB_SHOWDROPDOWN, 1, ByVal 0&)

End Sub

Sending messages in the GotFocus event for a control is a good technique because it enables you to avoid explicitly setting the focus to a control to get its hWnd.

Summary

This chapter was a whirlwind tour of event-driven programming and the use of events in
Visual Basic. I began with a description of what it means to be event-driven in comparison to using traditional techniques. After some warnings about potential pitfalls, you explored
application startup and shutdown. Then you saw the window and control events and how they applied to Visual Basic. Finally, you learned about some key Win32 functions that enable you to make use of Windows messages to get at some of the built-in Windows functionality that Visual Basic hides.

Previous Page TOC Next Page